#include "config.h"

#include "ms2sim.h"

ecm_uint32	cache_blksize = 4096;
ecm_uint32	n_caches = 1024;

const char	*bcache_mgmt_name;
char	*bcache_mod;

static ecm_ptrs_t	*bcaches;
static ecm_hashtable_t	hash_bcache;

static ecm_ptr_t	handle;

static ecm_uint32	n_active_bcaches;
static ecm_uint32	total_n_active_bcaches;

static ecm_uint64	total_n_accessed;
static ecm_uint64	total_n_hits;
static ecm_uint64	total_n_miss;

static ecm_uint64	total_n_hits_warmup;
static ecm_uint64	total_n_miss_warmup;

static ECMBOOL		cache_warmedup;

typedef struct {
	ecm_uint32 mid;
	ecm_uint32 bid;
} hkey_t;

blockcache_op_t	*bcache_op;

static bcache_t *
create_bcache(ecm_uint32 id)
{
	bcache_t	*bcache;

	bcache = (bcache_t *)ecm_malloc(sizeof(bcache_t));
	if (bcache == NULL) {
		ERR("bcache: out of memory");
		return NULL;
	}
	ecm_memset(bcache, 0, sizeof(bcache_t));
	bcache->id = id;
	INIT_LIST_HEAD(&bcache->waiters);

	return bcache;
}

static void
rehash_bcache(bcache_t *bcache, ecm_uint32 mid, ecm_uint32 bid)
{
	hkey_t	hkey;

	hkey.mid = bcache->mid;
	hkey.bid = bcache->bid;
	ecm_hashtable_remove(hash_bcache, sizeof(hkey), &hkey);

	hkey.mid = mid;
	hkey.bid = bid;
	ecm_hashtable_insert(hash_bcache, bcache, sizeof(hkey), &hkey);

	bcache->mid = mid;
	bcache->bid = bid;
	bcache->valid = ECM_FALSE;
}

bcache_t *
find_bcache(ecm_uint32 mid, ecm_uint32 bid)
{
	hkey_t	hkey = { mid, bid };

	return (bcache_t *)ecm_hashtable_get(hash_bcache, sizeof(hkey), &hkey);
}

/**
 * @brief get block cache
 *
 * returned bcache should be released by put_bcache()
 */
bcache_t *
get_bcache(session_t *session, ecm_uint32 bid)
{
	bcache_t	*bcache;
	ecm_uint32	mid = session->media->mid;

	ECMASSERT(list_empty(&session->list) && session->state == STATE_IDLE);

	bcache_op->access(session->sid, session->media->mid, bid);

	total_n_accessed++;
	if (total_n_accessed > n_caches)
		cache_warmedup = ECM_TRUE;

	bcache = find_bcache(mid, bid);
	if (bcache != NULL && bcache->valid) {
		if (is_stat_started) {
			total_n_hits++;
			if (cache_warmedup)
				total_n_hits_warmup++;
		}
		bcache_op->hit(bcache);
		n_active_bcaches++;
	}
	else {
		if (is_stat_started) {
			total_n_miss++;
			if (cache_warmedup)
				total_n_miss_warmup++;
		}
		if (bcache == NULL) {
			bcache = bcache_op->replace(mid, bid);
			if (bcache == NULL) {
				/* block cache shortage */
				ERR("cache overflow");
				ECMASSERT(0);
			}

			n_active_bcaches++;

			rehash_bcache(bcache, mid, bid);
			invoke_disk_IO(bcache, session->ts);
		}

		/* bcache is being loaded */
		list_add_tail(&session->list, &bcache->waiters);
		set_session_state(session, STATE_DISKIO);
	}

	bcache->refcnt++;
	return bcache;
}

void
put_bcache(session_t *session, bcache_t *bcache)
{
	ECMASSERT(bcache->refcnt > 0);
	bcache->refcnt--;
	if (bcache->refcnt == 0) {
		n_active_bcaches--;
		bcache_op->put(session->sid, bcache);
	}
}

void
complete_bcache_IO(bcache_t *bcache, ecm_uint64 ts)
{
	struct list_head	*lp, *next;
	session_t	*session;

	ECMASSERT(!bcache->valid);

	bcache->valid = ECM_TRUE;

	list_for_each_n (lp, &bcache->waiters, next) {
		session = list_entry(lp, session_t, list);
		ECMASSERT(session->state == STATE_DISKIO);
		set_session_state(session, STATE_IDLE);
		session->ts = ts;
		list_del_init(&session->list);
		put_session(session);
	}
}

void
notify_session_leave(session_t *session)
{
	bcache_op->leave(session->sid);
}

void
check_bcache_stats(void)
{
	total_n_active_bcaches += n_active_bcaches;
}

void
report_bcache_result(void)
{
	ecm_double	hit_ratio;
	ecm_double	avg_n_active_bcaches;
	ecm_uint64	total_n_warmup = total_n_hits_warmup + total_n_miss_warmup;

	avg_n_active_bcaches = (1.0 * total_n_active_bcaches) / stat_duration;
	hit_ratio = (1.0 * total_n_hits) / (total_n_hits + total_n_miss);

	printlog("Avg # of Active Block Cache: %.3lf\n", avg_n_active_bcaches);
	printlog("Cache Hit Ratio:             %.3lf%%\n", hit_ratio * 100);

	if (total_n_warmup > 0) {
		ecm_double	hit_ratio_warmup;

		hit_ratio_warmup = (1.0 * total_n_hits_warmup) / total_n_warmup;
		printlog("Cache Hit Ratio(Warm-up):    %.3lf%%\n", hit_ratio_warmup * 100);
	}
	else {
		printlog("Cache Hit Ratio(Warm-up):    -\n");
	}
}

static ECMBOOL
load_module(void)
{
	if ((handle = ecm_dlopen(bcache_mod, ECM_FALSE)) == NULL) {
		ERR("%s: bcache module loading failed", bcache_mod);
		return ECM_FALSE;
	}
	bcache_op = (blockcache_op_t *)ecm_dlsym(handle, "bcache_op");
	if (bcache_op == NULL) {
		ERR("%s: invalid bcache module", bcache_mod);
		ecm_dlclose(handle);
		handle = NULL;
		return ECM_FALSE;
	}
	if ((bcache_mgmt_name = bcache_op->init(cache_blksize, n_caches)) == NULL) {
		ERR("%s: cannot be initialized", bcache_mod);
		ecm_dlclose(handle);
		handle = NULL;
		return ECM_FALSE;
	}
	if (bcache_op->setup != NULL) {
		bcache_op->setup(get_mod_sr_conf(bcache_mgmt_name));
	}
	return ECM_TRUE;
}

ECMBOOL
init_bcache(void)
{
	bcache_t	*bcache;
	ecm_uint32	i;

	if (ecm_empty_str(bcache_mod)) {
		ERR("bcache_mod is empty");
		return ECM_FALSE;
	}

	if (!load_module()) {
		return ECM_FALSE;
	}

	bcaches = ecm_ptrs_new(n_caches);
	hash_bcache = ecm_hashtable_create(n_caches);

	for (i = 0; i < n_caches; i++) {
		if ((bcache = create_bcache(i + 1)) == NULL)
			return ECM_FALSE;
		bcache_op->create(bcache);
		ecm_ptrs_add(bcaches, bcache);
	}

	return ECM_TRUE;
}
